Day 6,Gemini 指出 Firestore Rules 可能允許跨帳號存取。
今天不再只看模型的文字判斷。我們會建立三種身分,直接向 Firestore Emulator 讀寫同一筆資料:
我們要回答一個具體問題:系統知道使用者是誰之後,是否也確認他有權操作這筆資料?
Authentication 是認證。系統透過密碼、Passkey 或身分提供者,確認使用者是誰。
Authorization 是授權。系統根據資源擁有者、角色或權限,決定使用者可以執行哪些操作。
假設公司替每位員工發識別證。警衛看過識別證後,確認訪客是公司員工。這是認證。
員工刷卡進入機房時,門禁系統還要確認他是否具備機房權限。這是授權。
登入成功只能通過第一關。它不能自動證明使用者可以讀取所有資料。
Day 5 把規則改成以下內容:
match /projects/{projectId} {
allow read, write: if request.auth != null;
}
request.auth != null 只確認請求來自已登入使用者。規則沒有比較登入者 UID 與文件中的 ownerId。
這代表:
projects 中的文件。第二項就是 Broken Access Control。系統完成認證,卻沒有執行資源層級的授權。
專案已加入以下測試腳本:
demo-app/tests/authorization-scenario.js
腳本會先建立一筆私人專案:
{
name: "Owner launch plan",
launchGoal: "Only the owner should read this document",
ownerId: "owner-user"
}
接著建立三個 Firestore Context:
const ownerDb = testEnvironment
.authenticatedContext("owner-user")
.firestore();
const attackerDb = testEnvironment
.authenticatedContext("attacker-user")
.firestore();
const guestDb = testEnvironment
.unauthenticatedContext()
.firestore();
這裡的 attacker-user 不具備管理員權限。他只是另一位正常登入的使用者。
腳本會執行四項操作:
進入 Demo 專案:
cd /media/mickey/777/ithome/demo-app
執行授權情境:
npm run authz:demo
實際結果如下:
Owner reads own project ALLOWED expected=ALLOWED
Guest reads owner's project DENIED expected=DENIED
Other user reads owner's project ALLOWED expected=DENIED
Other user edits owner's project ALLOWED expected=DENIED
訪客遭到拒絕,代表 Authentication 檢查確實生效。
另一位登入者卻能讀取與修改資料。這證明規則沒有檢查 Authorization。
這次測試使用已知的 Document ID 讀取文件。實際攻擊者也可能直接查詢集合,因為 Firestore 的 read 權限包含單筆讀取與列表查詢。前端畫面沒有顯示資料,不代表後端拒絕存取。攻擊者可以繞過畫面,直接使用 SDK 或 REST API 發出請求。
攻擊者只需要建立一般帳號。他不需要取得管理員角色,也不需要竊取其他人的密碼。
成功利用後,攻擊者可能:
嚴重度仍要根據資料內容判斷。如果文件只包含公開名稱,影響較低。如果文件包含個資、商業計畫或部署資訊,影響就會提高。
我們不能只根據規則寫法判定最終嚴重度。報告還要說明系統保存哪些資料,以及攻擊者能取得多少文件。
建立文件時,規則要檢查新資料中的 ownerId:
allow create: if request.auth != null
&& request.resource.data.ownerId == request.auth.uid;
讀取與刪除文件時,規則要檢查既有資料的擁有者:
allow read, delete: if request.auth != null
&& resource.data.ownerId == request.auth.uid;
更新文件時,規則還要禁止使用者改寫 ownerId:
allow update: if request.auth != null
&& resource.data.ownerId == request.auth.uid
&& request.resource.data.ownerId == resource.data.ownerId;
完整規則如下:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /projects/{projectId} {
allow create: if request.auth != null
&& request.resource.data.ownerId == request.auth.uid;
allow read, delete: if request.auth != null
&& resource.data.ownerId == request.auth.uid;
allow update: if request.auth != null
&& resource.data.ownerId == request.auth.uid
&& request.resource.data.ownerId == resource.data.ownerId;
}
}
}
Firestore Rules 使用兩種資料狀態:
resource.data 是資料庫目前保存的文件。request.resource.data 是寫入完成後的新文件。更新時只檢查舊資料不夠。否則擁有者可能把 ownerId 改成其他值,破壞資料的權限模型。
專案把修正版規則保存在:
review-target/secure-firestore.rules
執行:
npm run authz:secure
實際結果如下:
Owner reads own project ALLOWED expected=ALLOWED
Guest reads owner's project DENIED expected=DENIED
Other user reads owner's project DENIED expected=DENIED
Other user edits owner's project DENIED expected=DENIED
修正後,擁有者仍能操作自己的資料。訪客與其他登入者則遭到拒絕。
這項結果比「規則看起來比較安全」更有說服力。它證明四個指定情境都符合預期。
Day 6 的 Gemini Reviewer 只能閱讀規則。它可以指出授權條件過寬,卻不知道攻擊是否能在目前環境重現。
今天的測試補上兩項證據:
未來的 Agent 可以先分析程式碼,再呼叫測試工具驗證 Finding。報告也能附上實際指令與結果。
理想的 Finding 應包含:
問題:任何登入者都能存取其他使用者的專案
證據:allow read, write: if request.auth != null
前提:攻擊者具備一般登入帳號
驗證:npm run authz:demo
結果:跨帳號讀取與更新皆為 ALLOWED
影響:未授權資料讀取、竄改或刪除
修正:比較 request.auth.uid 與文件 ownerId
這種報告同時包含靜態證據與動態驗證。開發者不必只相信模型的判斷。
Authentication 回答「你是誰」,Authorization 回答「你能做什麼」。
request.auth != null 只能擋住未登入訪客。系統如果沒有繼續檢查資源擁有者或角色,任何登入者仍可能跨帳號操作資料。
今天也替 Production Readiness Agent 加入一項重要原則:
Agent 找到疑似漏洞後,應盡可能建立可重現測試。測試結果才能證明問題是否成立。
明天,我們會檢查 API、錯誤訊息與 Log,確認個人資料會不會在正常功能以外的地方外洩。